# make_report.py
import os
import json
import argparse
from typing import Dict, Any, List, Optional

from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.enum.shapes import MSO_SHAPE_TYPE
from pptx.enum.text import PP_PARAGRAPH_ALIGNMENT
from pptx.dml.color import RGBColor
from PIL import Image


def fit_image_to_shape(shape, image_path):
    # 保持比例，等比缩放，居中贴合到目标 shape 边界
    with Image.open(image_path) as im:
        img_w, img_h = im.size
    shp_w, shp_h = shape.width, shape.height
    scale = min(shp_w / img_w, shp_h / img_h)
    new_w, new_h = int(img_w * scale), int(img_h * scale)
    left = shape.left + (shp_w - new_w) // 2
    top = shape.top + (shp_h - new_h) // 2
    slide = shape.part.slide
    slide.shapes.add_picture(image_path, left, top, width=new_w, height=new_h)


def set_text(shape, text: str):
    tf = shape.text_frame
    tf.clear()
    lines = str(text).split("\n")
    for i, line in enumerate(lines):
        if i == 0:
            p = tf.paragraphs[0]
        else:
            p = tf.add_paragraph()
        p.text = line


def find_targets(slide):
    # 收集所有可匹配的形状，优先用 shape.name；其次用占位符类型名
    targets = {}
    for shape in slide.shapes:
        key = None
        if hasattr(shape, "name") and shape.name:
            key = shape.name
        if not key and getattr(shape, "is_placeholder", False):
            ph_type = str(shape.placeholder_format.type)
            key = ph_type.split(".")[-1]  # e.g. TITLE, BODY, PICTURE
        if key:
            targets.setdefault(key, []).append(shape)
    return targets


def fill_table(shape, table_data: Dict[str, Any]) -> bool:
    if not getattr(shape, "has_table", False):
        return False
    tbl = shape.table
    headers = table_data.get("headers", [])
    rows = table_data.get("rows", [])

    need_rows = 1 + len(rows)
    need_cols = max(len(headers), max((len(r) for r in rows), default=0))

    # 扩展表格至所需规模
    while len(tbl.rows) < need_rows:
        tbl.add_row()
    while len(tbl.columns) < need_cols and len(tbl.columns) > 0:
        tbl.add_column(tbl.columns[0].width)

    # 写表头
    for c, h in enumerate(headers):
        tbl.cell(0, c).text = str(h)

    # 写数据行
    for r_idx, r in enumerate(rows, start=1):
        for c, val in enumerate(r):
            tbl.cell(r_idx, c).text = str(val)
    return True


def try_fill_image(targets: Dict[str, List[Any]], key: str, value: str) -> bool:
    if not os.path.exists(value):
        return False
    # 先按 key 精确匹配，再回退到 PICTURE/OBJECT 可投图的框
    shapes = targets.get(key) or targets.get("PICTURE") or targets.get("OBJECT") or []
    if not shapes:
        # 再尝试查找具有图片外观的占位符名称
        for k, lst in targets.items():
            if k.upper().startswith("图片") or "PICTURE" in k.upper():
                shapes = lst
                break
    if not shapes:
        return False
    fit_image_to_shape(shapes[0], value)
    return True


def try_fill_text(targets: Dict[str, List[Any]], key: str, value: str) -> bool:
    # 精确名称 -> 首字母大写 -> TITLE/BODY 回退
    shapes = (
        targets.get(key)
        or targets.get(key.capitalize())
        or targets.get("TITLE")
        or targets.get("BODY")
        or []
    )
    for shp in shapes[:1]:
        if getattr(shp, "has_text_frame", False):
            set_text(shp, value)
            return True
    # 如果没匹配到，尝试随便找一个有文本框的 shape
    for lst in targets.values():
        for shp in lst:
            if getattr(shp, "has_text_frame", False):
                set_text(shp, value)
                return True
    return False


def fill_slide(slide, slide_content: Dict[str, Any]):
    targets = find_targets(slide)

    # 优先填表格（如果传了）
    for key, value in slide_content.items():
        if isinstance(value, dict) and ("headers" in value or "rows" in value):
            # 找第一个有 table 的 shape
            table_shapes = []
            for lst in targets.values():
                for sp in lst:
                    if getattr(sp, "has_table", False):
                        table_shapes.append(sp)
            if table_shapes:
                fill_table(table_shapes[0], value)

    # 填图片（支持 image, image1/image2...）
    img_keys = [k for k in slide_content.keys() if k.lower().startswith("image")]
    for k in sorted(img_keys):
        v = slide_content[k]
        if isinstance(v, str) and os.path.exists(v):
            try_fill_image(targets, k, v)

    # 填文本：跳过前面已处理的 dict/table 和 imageX
    for key, value in slide_content.items():
        if key in img_keys:
            continue
        if isinstance(value, dict) and ("headers" in value or "rows" in value):
            continue
        if isinstance(value, str):
            try_fill_text(targets, key, value)


def generate_report(template_pptx: str, structure_json: str, content_json: Optional[str], out_pptx: str):
    prs = Presentation(template_pptx)

    # 结构 JSON（来自模板提取信息，主要用于辅助匹配和回退策略）
    with open(structure_json, "r", encoding="utf-8") as f:
        structure = json.load(f)

    # 内容 JSON（每一页要填的文本/图片/表格）
    content = {}
    if content_json and os.path.exists(content_json):
        with open(content_json, "r", encoding="utf-8") as f:
            content = json.load(f)

    slides_data: List[Dict[str, Any]] = content.get("slides", [])
    repeat_layout_name = content.get("repeat_slide_layout")

    # 如果内容页数超过模板现有页数，复制某个 layout 来补
    layout_map = {l.name: l for l in prs.slide_layouts}
    while len(prs.slides) < len(slides_data):
        layout = layout_map.get(repeat_layout_name) or (prs.slide_layouts[1] if len(prs.slide_layouts) > 1 else prs.slide_layouts[0])
        prs.slides.add_slide(layout)

    # 逐页填充
    for i, slide_content in enumerate(slides_data):
        if i >= len(prs.slides):
            break
        slide = prs.slides[i]
        fill_slide(slide, slide_content)

    prs.save(out_pptx)
    print(f"Report generated: {out_pptx}")


def build_default_content_from_given_text_and_images() -> Dict[str, Any]:
    """
    可选：如果你不想单独写 content.json，这里内置一个示例把你提供的大段文字与图片按页分配。
    你可以根据需要调整每页的 key（如 TITLE、正文、image1 等），这些 key 会自动匹配模板中的 shape。
    """
    # 将长文分块
    page1_title = "VST清晰度问题调研与分析"
    page1_sub = "XR 产品设计开发部\nxxx年xx月xx日"

    page2_title = "目录"
    page2_body = "1. 背景\n2. 实验方法\n3. 实验项目\n4. 数据分析\n5. 竞品对比\n6. 总结"

    page3_title = "背景"
    page3_body = (
        "试产3机器量产后，测试发现清晰度随使用时长变差。虽经散热改造且sensor温度更低，但成像仍快速变糊。"
    )

    page4_title = "实验方法"
    page4_body = (
        "为稳定复现实验数据：\n"
        "1) 固定支架与图卡，使用vst视角重定位工具保证拍摄一致性；\n"
        "2) 控制基础温度，同一天交叉验证采样；\n"
        "3) 清晰度指标采用固定区域亮度标准差，自动化对齐/抠图计算；\n"
        "测试流程：固定眼镜位姿→冷机→上电采集热日志与VST→时间戳对齐→标注区域→生成曲线并分析。"
    )

    page5_title = "实验项目"
    page5_body = (
        "1) 同批次样机一致性（3台试产3，30分钟）；\n"
        "2) 单机重复测试趋势；\n"
        "3) 模组/盖板互换对比（试产1/3）；\n"
        "4) 不同温度下对比度损失及相关性；\n"
        "5) 相同温度下不同盖板对比度表现。"
    )

    page6_title = "数据分析要点"
    page6_body = (
        "1) 采集72组温度与图像数据，区域亮度标准差衡量对比度；\n"
        "2) 同批次样机初始对比度差异明显，模组体质差异大；\n"
        "3) 感知阈值约41，对比度低且温升衰减>10%时更易被察觉；\n"
        "4) 因子：盖板损失固定（丝印约-6.5%，镀膜约-12%）；模组初始对比度差异大（34-46）；\n"
        "   温升近似线性，每+10°C对比度约-2.55；\n"
        "   输出对比度≈初始对比度×盖板损失 − 衰减系数×温升。"
    )

    page7_title = "竞品对比"
    page7_body = (
        "YVR 10分钟后稳定在~10%衰减；Quest3s 未见明显衰减。\n"
        "YVR 初始对比度高且稳定后衰减小，与试产3优质模组+丝印盖板相当。"
    )

    page8_title = "总结"
    page8_body = (
        "1) 镀膜盖板使对比度-12%，多台试产3接近或低于阈值41，温升后更易感知变糊；\n"
        "2) 模组差异大，建议提升模组检测指标；\n"
        "3) 形成可复用的量化实验流程，利于后续问题分析。"
    )

    # 图片清单（你提供的路径）
    imgs = [
        r"D:/docs/vst_hot/charts/眼镜固定支架.png",
        r"D:/docs/vst_hot/charts/vst重定位工具.png",
        r"D:/docs/vst_hot/charts/VST对比度随温度变化趋势.png",
        r"D:/docs/vst_hot/charts/compare/yvr-compare.png",
        r"D:/docs/vst_hot/charts/compare/quest3s-compare.png",
        r"D:/docs/vst_hot/charts/试产3不同样机一致性测试-left.png",
        r"D:/docs/vst_hot/charts/试产3不同样机一致性测试-right.png",
        r"D:/docs/vst_hot/charts/试产3-不同盖板-相同温度-对比衰减测试-left.png",
        r"D:/docs/vst_hot/charts/试产3-不同盖板-相同温度-对比衰减测试-right.png",
    ]

    slides = [
        # 1 封面
        {"TITLE": page1_title, "BODY": page1_sub},
        # 2 目录
        {"TITLE": page2_title, "BODY": page2_body},
        # 3 背景 + 图
        {"TITLE": page3_title, "BODY": page3_body, "image1": imgs[0]},
        # 4 实验方法 + 图
        {"TITLE": page4_title, "BODY": page4_body, "image1": imgs[1]},
        # 5 实验项目
        {"TITLE": page5_title, "BODY": page5_body},
        # 6 数据分析 + 曲线图
        {"TITLE": page6_title, "BODY": page6_body, "image1": imgs[2]},
        # 7 竞品对比 + 图
        {"TITLE": page7_title, "BODY": page7_body, "image1": imgs[3], "image2": imgs[4]},
        # 8 总结 + 一致性对比图
        {"TITLE": page8_title, "BODY": page8_body, "image1": imgs[5], "image2": imgs[6]},
        # 9 备用：盖板相同温度衰减左右图
        {"TITLE": "附：不同盖板相同温度衰减", "BODY": "对比示意", "image1": imgs[7], "image2": imgs[8]},
    ]

    return {
        "repeat_slide_layout": "标题和内容",
        "slides": slides
    }


def main():
    parser = argparse.ArgumentParser(description="Fill PPTX with content and images")
    parser.add_argument("-t", "--template", required=True, help="Path to template .pptx")
    parser.add_argument("-s","--structure", required=True, help="Path to template structure JSON")
    parser.add_argument("-c","--content", help="Path to content JSON (optional)")
    parser.add_argument("-o","--out", default="report_out.pptx", help="Output pptx path")
    args = parser.parse_args()

    if args.content is None:
        tmp_content = build_default_content_from_given_text_and_images()
        tmp_json = os.path.join(os.path.dirname(args.out), "_auto_content.json")
        with open(tmp_json, "w", encoding="utf-8") as f:
            json.dump(tmp_content, f, ensure_ascii=False, indent=2)
        content_json = tmp_json
    else:
        content_json = args.content

    generate_report(args.template, args.structure, content_json, args.out)


if __name__ == "__main__":
    main()
